import { TransformControls } from 'three/examples/jsm/Addons.js';
import { Box3, Box3Helper, Clock, GridHelper, type Material, Vector2, Vector3 } from 'three/webgpu';

import type { Editor } from './Editor.ts';
import { EditorControls } from './EditorControls.ts';
import { initDispatchers } from './Viewport.dispatcher.ts';

const grid = new GridHelper(30, 30);
grid.material.color.setHex(0x999999);
grid.material.vertexColors = false;

// select box helper
const selectionBox = new Box3Helper(new Box3());
(selectionBox.material as Material).depthTest = false;
(selectionBox.material as Material).transparent = true;
selectionBox.visible = false;

export const initViewport = (editor: Editor) => {
  const { sceneHelpers } = editor;
  sceneHelpers.add(selectionBox);

  const render: () => void = createRenderFunction(editor);
  const animate: () => void = createAnimateFunction(editor, render);
  const { controls, transformControls } = initControls(editor, render);

  // events
  bindEvents(editor, render);

  // dispatchers
  initDispatchers({ editor, renderFn: render, animateFn: animate, controls, transformControls, selectionBox, grid });
};

const createRenderFunction = (editor: Editor) => {
  let startTime = 0;
  let endTime = 0;
  return async () => {
    //  const renderer = editor.renderer
    //  const targetElement = editor.targetElement
    const { targetElement, camera, scene, sceneHelpers, renderer } = editor;

    if (!renderer) return;
    startTime = performance.now();

    renderer.setViewport(0, 0, targetElement.offsetWidth, targetElement.offsetHeight);
    await renderer.renderAsync(scene, editor.viewportCamera);

    if (camera === editor.viewportCamera) {
      renderer.autoClear = false;
      if (grid.visible === true) await renderer.renderAsync(grid, camera);
      if (sceneHelpers.visible === true) await renderer.renderAsync(sceneHelpers, camera);
      renderer.autoClear = true;
    }

    endTime = performance.now();
    editor.signals.sceneRendered.dispatch(endTime - startTime);
  };
};

const createAnimateFunction = (editor: Editor, renderFn: () => void) => {
  let prevActionsInUse = 0;

  const clock = new Clock(); // only used for animations
  return () => {
    const mixer = editor.mixer;
    const delta = clock.getDelta();

    let needsUpdate = false;

    // Animations
    // @ts-expect-error
    const actions = mixer.stats.actions;

    if (actions.inUse > 0 || prevActionsInUse > 0) {
      prevActionsInUse = actions.inUse;

      mixer.update(delta);
      needsUpdate = true;

      if (editor.selected !== null) {
        editor.selected.updateWorldMatrix(false, true); // avoid frame late effect for certain skinned meshes (e.g. Michelle.glb)
        selectionBox.box.setFromObject(editor.selected, true); // selection box should reflect current animation state
      }
    }

    if (needsUpdate === true) renderFn();
  };
};

const initControls = (editor: Editor, renderFn: () => void) => {
  const { signals, camera, sceneHelpers } = editor;
  // vec3s for transform controls
  const objectPositionOnDown = new Vector3();
  const objectRotationOnDown = new Vector3();
  const objectScaleOnDown = new Vector3();

  const onControlsChange = () => {
    signals.cameraChanged.dispatch(camera);
    signals.refreshSidebarObject3D.dispatch(camera);
  };

  const controls = new EditorControls(camera);
  controls.addEventListener('change', onControlsChange);

  const transformControls = new TransformControls(camera);
  transformControls.addEventListener('axis-changed', () => {
    renderFn();
  });
  transformControls.addEventListener('objectChange', () => {
    signals.objectChanged.dispatch(transformControls.object);
  });
  transformControls.addEventListener('mouseDown', () => {
    const _object = transformControls.object;

    objectPositionOnDown.copy(_object.position);
    objectRotationOnDown.copy(_object.rotation);
    objectScaleOnDown.copy(_object.scale);

    controls.enabled = false;
  });
  transformControls.addEventListener('mouseUp', () => {
    const _object = transformControls.object;

    // if (_object !== undefined) {
    //   switch (transformControls.getMode()) {
    //     case 'translate':
    //       if (!objectPositionOnDown.equals(_object.position)) {
    //         editor.execute(new SetPositionCommand(editor, _object, _object.position, objectPositionOnDown));
    //       }

    //       break;

    //     case 'rotate':
    //       if (!objectRotationOnDown.equals(_object.rotation)) {
    //         editor.execute(new SetRotationCommand(editor, _object, _object.rotation, objectRotationOnDown));
    //       }

    //       break;

    //     case 'scale':
    //       if (!objectScaleOnDown.equals(_object.scale)) {
    //         editor.execute(new SetScaleCommand(editor, _object, _object.scale, objectScaleOnDown));
    //       }

    //       break;
    //   }
    // }

    controls.enabled = true;
  });

  sceneHelpers.add(transformControls.getHelper());

  return { controls, transformControls, objectPositionOnDown, objectRotationOnDown, objectScaleOnDown };
};

const bindEvents = (editor: Editor, renderFn: () => void) => {
  const { signals, selector, camera, targetElement } = editor;

  const onDownPosition = new Vector2();
  const onUpPosition = new Vector2();
  const onDoubleClickPosition = new Vector2();

  function handleClick() {
    if (onDownPosition.distanceTo(onUpPosition) === 0) {
      const intersects = selector.getPointerIntersects(onUpPosition, camera);
      signals.intersectionsDetected.dispatch(intersects);
      renderFn();
    }
  }

  function onmousedown(event: MouseEvent) {
    // event.preventDefault();

    const array = getMousePosition(targetElement, event.clientX, event.clientY);
    onDownPosition.fromArray(array);

    document.addEventListener('mouseup', onMouseUp);
  }

  function onMouseUp(event: MouseEvent) {
    const array = getMousePosition(targetElement, event.clientX, event.clientY);
    onUpPosition.fromArray(array);
    handleClick();
    document.removeEventListener('mouseup', onMouseUp);
  }

  function onTouchStart(event: TouchEvent) {
    const touch = event.changedTouches[0];
    const array = getMousePosition(targetElement, touch.clientX, touch.clientY);
    onDownPosition.fromArray(array);
    document.addEventListener('touchend', onTouchEnd);
  }

  function onTouchEnd(event: TouchEvent) {
    const touch = event.changedTouches[0];
    const array = getMousePosition(targetElement, touch.clientX, touch.clientY);
    onUpPosition.fromArray(array);
    handleClick();
    document.removeEventListener('touchend', onTouchEnd);
  }

  function onDoubleClick(event: MouseEvent) {
    const array = getMousePosition(targetElement, event.clientX, event.clientY);
    onDoubleClickPosition.fromArray(array);
    const intersects = selector.getPointerIntersects(onDoubleClickPosition, camera);
    if (intersects.length > 0) {
      const intersect = intersects[0];
      signals.objectFocused.dispatch(intersect.object);
    }
  }

  targetElement.addEventListener('mousedown', onmousedown);
  targetElement.addEventListener('touchstart', onTouchStart, { passive: false });
  targetElement.addEventListener('dblclick', onDoubleClick);
};

function getMousePosition(dom: HTMLElement, x: number, y: number) {
  const rect = dom.getBoundingClientRect();
  return [(x - rect.left) / rect.width, (y - rect.top) / rect.height];
}
